/*

Copyright (c) 2016-2017, Steven Siloti
Copyright (c) 2016-2018, Alden Torres
Copyright (c) 2018, d-komarov
Copyright (c) 2020-2021, Arvid Norberg
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/


/*

  The posix_part_file file format is an array of piece sized blocks with
  a simple header. For a given number of pieces, the header has a
  fixed size. The header size is rounded up to an even multiple of
  1024, in an attempt at improving disk I/O performance by aligning
  reads and writes to clusters on the drive. This is the file header
  format. All values are stored big endian on disk.


  // the size of the torrent (and can be used to calculate the size
  // of the file header)
  uint32_t num_pieces;

  // the number of bytes in each piece. This determines the size of
  // each slot in the part file. This is typically an even power of 2,
  // but it is not guaranteed to be.
  uint32_t piece_size;

  // this is an array specifying which slots a particular piece resides in,
  // A value of 0xffffffff (-1 if you will) means the piece is not in the posix_part_file
  // Any other value means the piece resides in the slot with that index
  uint32_t piece[num_pieces];

  // unused, n is defined as the number to align the size of this
  // header to an even multiple of 1024 bytes.
  uint8_t padding[n];

*/

#include "libtorrent/aux_/posix_part_file.hpp"
#include "libtorrent/aux_/numeric_cast.hpp"
#include "libtorrent/aux_/io_bytes.hpp"
#include "libtorrent/assert.hpp"
#include "libtorrent/aux_/vector.hpp"
#include "libtorrent/aux_/path.hpp"
#include "libtorrent/aux_/storage_utils.hpp" // for copy_file

#include <functional> // for std::function
#include <cstdint>

namespace {

	// round up to even kilobyte
	int round_up(int n)
	{ return (n + 1023) & ~0x3ff; }
}

namespace libtorrent {
namespace aux {

	posix_part_file::posix_part_file(std::string path, std::string name
		, int const num_pieces, int const piece_size)
		: m_path(std::move(path))
		, m_name(std::move(name))
		, m_max_pieces(num_pieces)
		, m_piece_size(piece_size)
		, m_header_size(round_up((2 + num_pieces) * 4))
	{
		TORRENT_ASSERT(num_pieces > 0);
		TORRENT_ASSERT(m_piece_size > 0);

		error_code ec;
		auto f = open_file(open_mode::read_only, ec);
		if (ec) return;

		// parse header
		std::vector<char> header(static_cast<std::size_t>(m_header_size));
		auto const n = std::fread(header.data(), 1, header.size(), f.file());
		if (std::size_t(n) != header.size())
		{
			ec.assign(errno, generic_category());
			return;
		}

		// we don't have a full header. consider the file empty
		if (n < std::size_t(m_header_size)) return;
		using namespace libtorrent::aux;

		char* ptr = header.data();
		// we have a header. Parse it
		int const num_pieces_ = int(read_uint32(ptr));
		int const piece_size_ = int(read_uint32(ptr));

		// if there is a mismatch in number of pieces or piece size
		// consider the file empty and overwrite anything in there
		if (num_pieces != num_pieces_ || m_piece_size != piece_size_) return;

		// this is used to determine which slots are free, and how many
		// slots are allocated
		aux::vector<bool, slot_index_t> free_slots;
		free_slots.resize(num_pieces, true);

		for (auto i = piece_index_t(0); i < piece_index_t(num_pieces); ++i)
		{
			slot_index_t const slot(read_int32(ptr));
			if (static_cast<int>(slot) < 0) continue;

			// invalid part-file
			TORRENT_ASSERT(slot < slot_index_t(num_pieces));
			if (slot >= slot_index_t(num_pieces)) continue;

			if (slot >= m_num_allocated)
				m_num_allocated = next(slot);

			free_slots[slot] = false;
			m_piece_map[i] = slot;
		}

		// now, populate the free_list with the "holes"
		for (slot_index_t i(0); i < m_num_allocated; ++i)
		{
			if (free_slots[i]) m_free_slots.push_back(i);
		}
	}

	posix_part_file::~posix_part_file()
	{
		error_code ec;
		flush_metadata_impl(ec);
	}

	slot_index_t posix_part_file::allocate_slot(piece_index_t const piece)
	{
		TORRENT_ASSERT(m_piece_map.find(piece) == m_piece_map.end());
		slot_index_t slot(-1);
		if (!m_free_slots.empty())
		{
			slot = m_free_slots.front();
			m_free_slots.erase(m_free_slots.begin());
		}
		else
		{
			slot = m_num_allocated;
			++m_num_allocated;
		}

		m_piece_map[piece] = slot;
		m_dirty_metadata = true;
		return slot;
	}

	int posix_part_file::write(span<char const> buf, piece_index_t const piece
		, int const offset, error_code& ec)
	{
		TORRENT_ASSERT(offset >= 0);
		TORRENT_ASSERT(int(buf.size()) + offset <= m_piece_size);

		auto f = open_file(open_mode::read_write, ec);
		if (ec) return -1;

		auto const i = m_piece_map.find(piece);
		slot_index_t const slot = (i == m_piece_map.end())
			? allocate_slot(piece) : i->second;

		if (portable_fseeko(f.file(), slot_offset(slot) + offset, SEEK_SET) != 0)
		{
			ec.assign(errno, generic_category());
			return -1;
		}
		auto const written = std::fwrite(buf.data(), 1, std::size_t(buf.size()), f.file());
		if (written != std::size_t(buf.size()))
		{
			ec.assign(errno, generic_category());
			return -1;
		}
		return int(written);
	}

	int posix_part_file::read(span<char> buf
		, piece_index_t const piece
		, int const offset, error_code& ec)
	{
		TORRENT_ASSERT(offset >= 0);
		TORRENT_ASSERT(int(buf.size()) + offset <= m_piece_size);

		auto const i = m_piece_map.find(piece);
		if (i == m_piece_map.end())
		{
			ec = make_error_code(boost::system::errc::no_such_file_or_directory);
			return -1;
		}

		slot_index_t const slot = i->second;

		auto f = open_file(open_mode::read_only, ec);
		if (ec) return -1;

		if (portable_fseeko(f.file(), slot_offset(slot) + offset, SEEK_SET) != 0)
		{
			ec.assign(errno, generic_category());
			return -1;
		}
		auto const read = std::fread(buf.data(), 1, std::size_t(buf.size()), f.file());
		if (read != std::size_t(buf.size()))
		{
			if (std::ferror(f.file())) ec.assign(errno, generic_category());
			else ec.assign(errors::file_too_short, libtorrent_category());
			return -1;
		}
		return int(read);
	}

	int posix_part_file::hash(hasher& ph
		, std::ptrdiff_t const len
		, piece_index_t const piece
		, int const offset, error_code& ec)
	{
		return do_hash(ph, len, piece, offset, ec);
	}

	int posix_part_file::hash2(hasher256& ph
		, std::ptrdiff_t const len
		, piece_index_t const piece
		, int const offset, error_code& ec)
	{
		return do_hash(ph, len, piece, offset, ec);
	}

	template <typename Hasher>
	int posix_part_file::do_hash(Hasher& ph
		, std::ptrdiff_t const len
		, piece_index_t const piece
		, int const offset, error_code& ec)
	{
		TORRENT_ASSERT(offset >= 0);
		TORRENT_ASSERT(len >= 0);
		TORRENT_ASSERT(int(len) + offset <= m_piece_size);

		auto const i = m_piece_map.find(piece);
		if (i == m_piece_map.end())
		{
			ec = error_code(boost::system::errc::no_such_file_or_directory
				, boost::system::generic_category());
			return -1;
		}

		slot_index_t const slot = i->second;
		auto f = open_file(open_mode::read_only, ec);
		if (ec) return -1;

		std::vector<char> buffer(static_cast<std::size_t>(len));
		std::int64_t const slot_offset = std::int64_t(m_header_size) + std::int64_t(static_cast<int>(slot)) * m_piece_size;
		if (portable_fseeko(f.file(), slot_offset + offset, SEEK_SET) != 0)
		{
			ec.assign(errno, generic_category());
			return -1;
		}
		auto const ret = std::fread(buffer.data(), 1, buffer.size(), f.file());
		if (ret != buffer.size())
		{
			ec.assign(errno, generic_category());
			return -1;
		}
		ph.update(buffer);
		return numeric_cast<int>(ret);
	}

	file_pointer posix_part_file::open_file(open_mode const mode, error_code& ec)
	{
		std::string const fn = combine_path(m_path, m_name);
#ifdef TORRENT_WINDOWS
		wchar_t const* mode_str[] = {L"rb", L"rb+"};
		file_pointer ret(::_wfopen(convert_to_native_path_string(fn).c_str()
			, mode_str[static_cast<std::uint8_t>(mode)]));
#else
		char const* mode_str[] = {"rb", "rb+"};
		file_pointer ret(::fopen(fn.c_str()
			, mode_str[static_cast<std::uint8_t>(mode)]));
#endif
		if (ret.file() == nullptr)
			ec.assign(errno, generic_category());

		if (mode == open_mode::read_write
			&& ec == boost::system::errc::no_such_file_or_directory)
		{
			// this means the directory the file is in doesn't exist.
			// so create it
			ec.clear();
			create_directories(m_path, ec);

			if (ec) return {};

#ifdef TORRENT_WINDOWS
			ret = file_pointer(::_wfopen(convert_to_native_path_string(fn).c_str(), L"wb+"));
#else
			ret = file_pointer(::fopen(fn.c_str(), "wb+"));
#endif
			if (ret.file() == nullptr)
				ec.assign(errno, generic_category());
		}
		if (ec) return {};
		return ret;
	}

	void posix_part_file::free_piece(piece_index_t const piece)
	{
		auto const i = m_piece_map.find(piece);
		if (i == m_piece_map.end()) return;

		// TODO: what do we do if someone is currently reading from the disk
		// from this piece? does it matter? Since we won't actively erase the
		// data from disk, but it may be overwritten soon, it's probably not that
		// big of a deal

		m_free_slots.push_back(i->second);
		m_piece_map.erase(i);
		m_dirty_metadata = true;
	}

	void posix_part_file::move_partfile(std::string const& path, error_code& ec)
	{
		flush_metadata_impl(ec);
		if (ec) return;

		if (!m_piece_map.empty())
		{
			std::string old_path = combine_path(m_path, m_name);
			std::string new_path = combine_path(path, m_name);

			rename(old_path, new_path, ec);
			if (ec == boost::system::errc::no_such_file_or_directory)
				ec.clear();

			if (ec)
			{
				storage_error se;
				aux::copy_file(old_path, new_path, se);
				ec = se.ec;
				if (ec) return;
				remove(old_path, ec);
			}
		}
		m_path = path;
	}

	void posix_part_file::export_file(std::function<void(std::int64_t, span<char>)> f
		, std::int64_t const offset, std::int64_t size, error_code& ec)
	{
		// there's nothing stored in the posix_part_file. Nothing to do
		if (m_piece_map.empty()) return;

		piece_index_t piece(int(offset / m_piece_size));
		piece_index_t const end = piece_index_t(int(((offset + size) + m_piece_size - 1) / m_piece_size));

		std::unique_ptr<char[]> buf;

		std::int64_t piece_offset = offset - std::int64_t(static_cast<int>(piece))
			* m_piece_size;
		std::int64_t file_offset = 0;
		auto file = open_file(open_mode::read_only, ec);
		if (ec) return;

		for (; piece < end; ++piece)
		{
			auto const i = m_piece_map.find(piece);
			int const block_to_copy = int(std::min(m_piece_size - piece_offset, size));
			if (i != m_piece_map.end())
			{
				slot_index_t const slot = i->second;

				if (!buf) buf.reset(new char[std::size_t(m_piece_size)]);

				if (portable_fseeko(file.file(), slot_offset(slot) + piece_offset, SEEK_SET) != 0)
				{
					ec.assign(errno, generic_category());
					return;
				}
				auto bytes_read = std::fread(buf.get(), 1, std::size_t(block_to_copy), file.file());
				if (int(bytes_read) != block_to_copy)
					ec.assign(errno, generic_category());

				if (ec) return;

				f(file_offset, {buf.get(), block_to_copy});
			}
			file_offset += block_to_copy;
			piece_offset = 0;
			size -= block_to_copy;
		}
	}

	void posix_part_file::flush_metadata(error_code& ec)
	{
		flush_metadata_impl(ec);
	}

	// TODO: instead of rebuilding the whole file header
	// and flushing it, update the slot entries as we go
	void posix_part_file::flush_metadata_impl(error_code& ec)
	{
		// do we need to flush the metadata?
		if (m_dirty_metadata == false) return;

		if (m_piece_map.empty())
		{
			// if we don't have any pieces left in the
			// part file, remove it
			std::string const p = combine_path(m_path, m_name);
			remove(p, ec);

			if (ec == boost::system::errc::no_such_file_or_directory)
				ec.clear();
			return;
		}

		auto f = open_file(open_mode::read_write, ec);
		if (ec) return;

		std::vector<char> header(static_cast<std::size_t>(m_header_size));

		using namespace libtorrent::aux;

		char* ptr = header.data();
		write_uint32(m_max_pieces, ptr);
		write_uint32(m_piece_size, ptr);

		for (piece_index_t piece(0); piece < piece_index_t(m_max_pieces); ++piece)
		{
			auto const i = m_piece_map.find(piece);
			slot_index_t const slot(i == m_piece_map.end()
				? slot_index_t(-1) : i->second);
			write_int32(static_cast<int>(slot), ptr);
		}
		std::memset(ptr, 0, std::size_t(m_header_size - (ptr - header.data())));

		auto const written = std::fwrite(header.data(), 1, header.size(), f.file());
		if (written != header.size())
		{
			ec.assign(errno, generic_category());
			return;
		}
		m_dirty_metadata = false;
	}
}
}
